/*
 * Copyright 2002-2005 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lib.jsonrpc;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.cglib.core.ReflectUtils;

import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;


// TODO: Auto-generated Javadoc
/**
 * The Class Utils includes static help methods.
 * 
 * 
 * 
 * @author jay liu
 * @since 02.10.2010
 */
public abstract class Utils {
	
	/** The Constant NULL. */
	public final static String NULL = "_nil";

	/** The props. */
	private static Map<Class<?>, List<Field>> props = new HashMap<Class<?>, List<Field>>();

	/**
	 * Gets the jSON property.
	 *
	 * @param clazzType the clazz type
	 * @param fieldType the field type
	 * @param json the json
	 * @param name the name
	 * @param factory the factory
	 * @return the jSON property
	 * @throws JSONException the jSON exception
	 * @throws InstantiationException the instantiation exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	@SuppressWarnings("unchecked")
	public static Object getJSONProperty(Type clazzType, Type fieldType,
			JSONObject json, String name, ObjectFactory factory)
			throws JSONException, InstantiationException,
			IllegalAccessException, InvocationTargetException {
		if (json.isNull(name))
			return null;

		if (fieldType instanceof Class<?>) {
			Class<?> clazz = (Class<?>) fieldType;

			if (clazz.equals(Void.class)) {
				return clazz.newInstance();
			}

			if (clazz.equals(String.class))
				return json.optString(name);
			else if (clazz.equals(Boolean.class))
				return json.optBoolean(name);
			else if (clazz.equals(Integer.class))
				return json.optInt(name);
			else if (clazz.equals(Long.class))
				return json.optLong(name);
			else if (clazz.equals(Double.class))
				return json.optDouble(name);
			else if (clazz.equals(Date.class))
				return new Date(json.optLong(name));
			else if (clazz.equals(Float.class))
				return new Double(json.optDouble(name)).floatValue();
			else {
				Object obj = factory.newInstance(clazz);
				fromJSON(fieldType, obj, json.optJSONObject(name), factory);
				return obj;
			}

		} else if (fieldType instanceof ParameterizedType) {

			ParameterizedType p = (ParameterizedType) fieldType;
			Class<?> clazzRaw = (Class<?>) p.getRawType();
			Type[] targs = p.getActualTypeArguments();
			Type type = targs[0];
			if (type instanceof TypeVariable) {
				if (!(clazzType instanceof ParameterizedType)) {
					// field is generic, so type info come from object
					throw new JSONException("type is not parameterizedType");
				}
				p = (ParameterizedType) clazzType;
				type = p.getActualTypeArguments()[0];
			}

			if (Collection.class.isAssignableFrom(clazzRaw)) {

				if (targs.length == 1) {

					ArrayList list = new ArrayList();
					JSONArray ja = json.getJSONArray(name);
					for (int i = 0; i < ja.length(); i++) {
						Object oo = getJSONArrayObject(type, ja, i, factory);
						list.add(oo);
					}
					return list;

				} else {
					// Collection generic only has 1 parameter...
					throw new JSONException(
							"Only one Generic parameter supported");
				}

			} else {
				Object obj = factory.newInstance(clazzRaw);
				fromJSON(clazzType, obj, json.optJSONObject(name), factory);
				return obj;
			}

		} else {
			throw new JSONException("JSON format exception");
		}

	}

	/**
	 * Gets the jSON array object.
	 *
	 * @param type the type
	 * @param jarray the jarray
	 * @param index the index
	 * @param factory the factory
	 * @return the jSON array object
	 * @throws JSONException the jSON exception
	 * @throws InstantiationException the instantiation exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	@SuppressWarnings("unchecked")
	public static Object getJSONArrayObject(Type type, JSONArray jarray,
			int index, ObjectFactory factory) throws JSONException,
			InstantiationException, IllegalAccessException,
			InvocationTargetException {
		if (jarray.isNull(index))
			return null;
		if (type instanceof Class<?>) {
			Class<?> clazz = (Class<?>) type;
			if (clazz.equals(String.class))
				return jarray.optString(index);
			else if (clazz.equals(Boolean.class))
				return jarray.optBoolean(index);
			else if (clazz.equals(Integer.class))
				return jarray.optInt(index);
			else if (clazz.equals(Long.class))
				return jarray.optLong(index);
			else if (clazz.equals(Double.class))
				return jarray.optDouble(index);
			else if (clazz.equals(Date.class))
				return new Date(jarray.optLong(index));
			else if (clazz.equals(Float.class))
				return new Double(jarray.optDouble(index)).floatValue();
			else {
				Object obj = factory.newInstance(clazz);
				fromJSON(type, obj, jarray.getJSONObject(index), factory);
				return obj;
			}
		} else if (type instanceof ParameterizedType) {
			ParameterizedType p = (ParameterizedType) type;
			Class<?> clazzRaw = (Class<?>) p.getRawType();
			if (Collection.class.isAssignableFrom(clazzRaw)) {
				Type[] targs = p.getActualTypeArguments();

				if (targs.length == 1) {

					ArrayList list = new ArrayList();
					JSONArray ja = jarray.getJSONArray(index);
					for (int i = 0; i < ja.length(); i++) {
						Object oo = getJSONArrayObject(targs[0], ja, i, factory);
						list.add(oo);
					}
					return list;
				} else {
					// Collection generic only has 1 parameter...
					throw new JSONException(
							"Collection generic only support 1 parameter");
				}
			} else {
				Object obj = factory.newInstance(clazzRaw);
				fromJSON(type, obj, jarray.getJSONObject(index), factory);
				return obj;
			}
		} else {
			throw new JSONException("json format exception");
		}

	}

	/**
	 * Put json property.
	 *
	 * @param clazzType the clazz type
	 * @param fieldType the field type
	 * @param json the json
	 * @param name the name
	 * @param value the value
	 * @throws JSONException the jSON exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	@SuppressWarnings("unchecked")
	public static void putJSONProperty(Type clazzType, Type fieldType,
			JSONObject json, String name, Object value) throws JSONException,
			IllegalAccessException, InvocationTargetException {

		if (value == null) {
			json.put(name, (Object) null);
			return;
		}

		if (fieldType instanceof Class<?>) {
			Class<?> clazz = (Class<?>) fieldType;
			if (clazz.equals(String.class)) {
				json.put(name, value);
				return;
			} else if (clazz.equals(Boolean.class)) {
				json.put(name, value);
				return;
			} else if (clazz.equals(Integer.class)) {
				json.put(name, value);
				return;
			} else if (clazz.equals(Long.class)) {
				json.put(name, value);
				return;
			} else if (clazz.equals(Double.class)) {
				json.put(name, value);
				return;
			} else if (clazz.equals(Date.class)) {
				Date val = (Date) value;
				json.put(name, val.getTime());
				return;
			} else if (clazz.equals(Float.class)) {
				json.put(name, value);
				return;

			} else {

				JSONObject jobj = toJSON(fieldType, value);
				json.put(name, jobj);
				return;

			}
		} else if (fieldType instanceof ParameterizedType) {
			ParameterizedType p = (ParameterizedType) fieldType;
			Class<?> clazzRaw = (Class<?>) p.getRawType();
			Type[] targs = p.getActualTypeArguments();
			Type type = targs[0];
			if (type instanceof TypeVariable) {
				if (!(clazzType instanceof ParameterizedType)) {
					// field is generic, so type info come from object
					throw new JSONException(
							"Generic Collection support only 1 paramter");
				}
				p = (ParameterizedType) clazzType;
				type = p.getActualTypeArguments()[0];
			}

			if (Collection.class.isAssignableFrom(clazzRaw)) {

				if (targs.length == 1) {

					Collection objs = (Collection) value;
					JSONArray ja = new JSONArray();
					int i = 0;
					for (Object o : objs) {
						putObjectToJSONArray(type, ja, i, o);
						i++;
					}

					json.put(name, ja);
					return;

				} else {
					// Collection generic only has 1 parameter...
					throw new JSONException(
							"Collectoin generic only supports 1 parameter");
				}

			} else {

				JSONObject jobj = toJSON(clazzType, value);
				json.put(name, jobj);
				return;
			}
		} else {
			throw new JSONException("json format exception");
		}

	}

	/**
	 * Put object to json array.
	 *
	 * @param type the type
	 * @param jarray the jarray
	 * @param index the index
	 * @param obj the obj
	 * @throws JSONException the jSON exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	@SuppressWarnings("unchecked")
	public static void putObjectToJSONArray(Type type, JSONArray jarray,
			int index, Object obj) throws JSONException,
			IllegalAccessException, InvocationTargetException {

		if (obj == null) {
			jarray.put(index, (Object) null);
			return;
		}

		if (type instanceof Class<?>) {
			Class<?> clazz = (Class<?>) type;
			if (clazz.equals(String.class)) {
				jarray.put(index, obj);
				return;
			} else if (clazz.equals(Boolean.class)) {
				jarray.put(index, obj);
				return;
			} else if (clazz.equals(Integer.class)) {
				jarray.put(index, obj);
				return;
			} else if (clazz.equals(Long.class)) {
				jarray.put(index, obj);
				return;
			} else if (clazz.equals(Double.class)) {
				jarray.put(index, obj);
				return;
			} else if (clazz.equals(Date.class)) {
				jarray.put(index, obj);
				return;
			} else if (clazz.equals(Float.class)) {
				jarray.put(index, obj);
				return;
			} else {

				JSONObject json = toJSON(type, obj);
				jarray.put(index, json);
				return;
			}
		} else if (type instanceof ParameterizedType) {
			ParameterizedType p = (ParameterizedType) type;
			Class<?> clazzRaw = (Class<?>) p.getRawType();
			if (Collection.class.isAssignableFrom(clazzRaw)) {
				Type[] targs = p.getActualTypeArguments();

				if (targs.length == 1) {
					Collection objs = (Collection) obj;

					JSONArray ja = new JSONArray();
					int i = 0;
					for (Object o : objs) {
						putObjectToJSONArray(targs[0], ja, i, o);
						jarray.put(index, ja);
						i++;
					}
					return;
				} else {
					// Collection generic only has 1 parameter...
					throw new JSONException(
							"Collection generic only supports 1 parameter");
				}
			} else {
				JSONObject json = toJSON(type, obj);
				jarray.put(index, json);
				return;
			}

		} else {
			throw new JSONException("json format excpetion");
		}

	}

	/**
	 * To json.
	 *
	 * @param clazzType the clazz type
	 * @param obj the obj
	 * @return the jSON object
	 * @throws JSONException the jSON exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	public static JSONObject toJSON(Type clazzType, Object obj)
			throws JSONException, IllegalAccessException,
			InvocationTargetException {
		Class<?> clazz = obj.getClass();
		Annotation a = clazz.getAnnotation(JSONObj.class);
		if (a == null)
			return null;

		List<Field> fs = getFields(clazz);
		JSONObject ret = new JSONObject();

		for (Field f : fs) {
			String name = f.getName();
			Object val = getPropertyValue(obj, name);

			Type ft = f.getGenericType();

			if (val != null && Date.class.isAssignableFrom(f.getType()))
				ret.put(name, ((Date) val).getTime());
			else {
				if (val != null)
					putJSONProperty(clazzType, ft, ret, name, val);
				else
					ret.put(name, (Object) null);
			}
		}

		return ret;
	}

	/**
	 * From json.
	 *
	 * @param clazzType the clazz type
	 * @param obj the obj
	 * @param json the json
	 * @param factory the factory
	 * @throws JSONException the jSON exception
	 * @throws InstantiationException the instantiation exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	public static void fromJSON(Type clazzType, Object obj, JSONObject json,
			ObjectFactory factory) throws JSONException,
			InstantiationException, IllegalAccessException,
			InvocationTargetException {
		Class<?> clazz = obj.getClass();
		Annotation a = clazz.getAnnotation(JSONObj.class);
		if (a == null)
			return;

		List<Field> fs = getFields(clazz);

		for (Field f : fs) {
			String name = f.getName();
			Type ft = f.getGenericType();

			if (json.has(name)) {
				Object val = getJSONProperty(clazzType, ft, json, name, factory);
				if (val != null && Date.class.isAssignableFrom(f.getType()))
					setPropertyValue(obj, name, new Date(json.getLong(name)));
				else {

					setPropertyValue(obj, name, val);
				}

			}
		}

	}

	/**
	 * Copy json properties.
	 *
	 * @param target the target
	 * @param src the src
	 * @param excludes the excludes
	 * @param nullable the nullable
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	public static void copyJSONProperties(Object target, Object src,
			Collection<String> excludes, boolean nullable)
			throws IllegalAccessException, InvocationTargetException {
		Class<?> clazzSrc = src.getClass();
		Class<?> clazzTarget = src.getClass();
		Annotation a = clazzSrc.getAnnotation(JSONObj.class);
		if (a == null)
			return;

		a = clazzTarget.getAnnotation(JSONObj.class);
		if (a == null)
			return;

		List<Field> fs = getFields(clazzSrc);

		for (Field f : fs) {
			String name = f.getName();
			if (!excludes.contains(name)) {
				Object value = getPropertyValue(src, name);
				if (nullable || value != null) {
					setPropertyValue(target, name, value);
				}
			}
		}
	}

	/**
	 * Gets the fields.
	 *
	 * @param clazz the clazz
	 * @return the fields
	 */
	public static List<Field> getFields(Class<?> clazz) {

		if (props.containsKey(clazz))
			return props.get(clazz);

		ArrayList<Field> fieldList = new ArrayList<Field>();
		Field[] fs = clazz.getDeclaredFields();
		for (Field f : fs) {
			Annotation fa = f.getAnnotation(JSONProperty.class);
			if (fa != null) {
				fieldList.add(f);
			}
		}

		props.put(clazz, fieldList);

		return fieldList;
	}

  /**
   * Gets the bean properties descriptors of target class.
   * 
   * @param clazz the target class
   * @return the PropertyDescriptor array for target class
   * @throws InvocationTargetException the invocation target exception
   */
  private static PropertyDescriptor[] getBeanProperties(final Class clazz) throws InvocationTargetException{
    try{
      return Introspector.getBeanInfo(clazz).getPropertyDescriptors();
    }catch(IntrospectionException ie){
      throw new InvocationTargetException(ie, ie.getMessage());
    }
  }

	/**
	 * Gets the property value.
	 *
	 * @param obj the obj
	 * @param name the name
	 * @return the property value
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	public static Object getPropertyValue(Object obj, String name)
			throws IllegalAccessException, InvocationTargetException {

		//PropertyDescriptor[] pds = ReflectUtils.getBeanProperties(obj
		//		.getClass());
    /**
     * This patch will improve performance. Call of ReflectUtils.getBeanProperties() is very slow, because inside each
     * time using a very slow call Introspector.getBeanInfo(type, Object.class).
     */
		PropertyDescriptor[] pds = getBeanProperties(obj.getClass());

		for (PropertyDescriptor pd : pds) {
			if (pd.getName().equals(name)) {
				Method m = pd.getReadMethod();
				if (m != null)
					return m.invoke(obj, new Object[0]);
			}
		}

		return null;
	}

	/**
	 * Sets the property value.
	 *
	 * @param obj the obj
	 * @param name the name
	 * @param value the value
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	public static void setPropertyValue(Object obj, String name, Object value)
			throws IllegalAccessException, InvocationTargetException {
		//PropertyDescriptor[] pds = ReflectUtils.getBeanProperties(obj
		//		.getClass());
    /**
     * This patch will improve performance. Call of ReflectUtils.getBeanProperties() is very slow, because inside each
     * time using a very slow call Introspector.getBeanInfo(type, Object.class).
     */
		PropertyDescriptor[] pds = getBeanProperties(obj.getClass());

		for (PropertyDescriptor pd : pds) {
			if (pd.getName().equals(name)) {
				Method m = pd.getWriteMethod();
				if (m != null)
					m.invoke(obj, value);

			}
		}

	}

	/**
	 * Parses the return value.
	 *
	 * @param method the method
	 * @param pkt the pkt
	 * @param factory the factory
	 * @return the object
	 * @throws JSONException the jSON exception
	 * @throws InstantiationException the instantiation exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	protected static Object parseReturnValue(Method method, JSONObject pkt,
			lib.jsonrpc.ObjectFactory factory) throws JSONException,
			InstantiationException, IllegalAccessException,
			InvocationTargetException {
		if (pkt.has("error")) {
			String errorMessage = pkt.optString("message");
			long errorCode = pkt.optInt("code");
			String errorData = pkt.optString("data");
			RPCException e = new RPCException(errorCode, errorMessage);
			e.setData(errorData);
			throw e;
		}

		Type type = method.getGenericReturnType();
		Object obj = getJSONProperty(type, type, pkt,
				"result", factory);
		return obj;

	}

	/**
	 * Encode return value.
	 *
	 * @param method the method
	 * @param obj the obj
	 * @return the jSON object
	 * @throws JSONException the jSON exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	protected static JSONObject encodeReturnValue(Method method, Object obj)
			throws JSONException, IllegalAccessException,
			InvocationTargetException {
		Type type = method.getGenericReturnType();

		JSONObject pkt = new JSONObject();

		if (obj == null) {
			pkt.put("result", (Object) null);
			return pkt;
		}

		if (type instanceof Class<?>) {
			Class<?> pcl = (Class<?>) type;
			if (pcl.equals(Void.class)) {
				return pkt;
			}
		}

		putJSONProperty(type, type, pkt, "result", obj);
		return pkt;

	}

	/**
	 * Parses the arguments.
	 *
	 * @param method the method
	 * @param jargs the jargs
	 * @param factory the factory
	 * @return the object[]
	 * @throws InstantiationException the instantiation exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws JSONException the jSON exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	protected static Object[] parseArguments(Method method, JSONArray jargs,
			lib.jsonrpc.ObjectFactory factory) throws InstantiationException,
			IllegalAccessException, JSONException, InvocationTargetException {
		Type[] argTypes = method.getGenericParameterTypes();
		Object[] args = new Object[argTypes.length];

		for (int i = 0; i < argTypes.length; i++) {
			Object obj = lib.jsonrpc.Utils.getJSONArrayObject(argTypes[i],
					jargs, i, factory);
			args[i] = obj;
		}

		return args;

	}

	/**
	 * Encode arguments.
	 *
	 * @param method the method
	 * @param args the args
	 * @return the jSON array
	 * @throws JSONException the jSON exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	protected static JSONArray encodeArguments(Method method, Object[] args)
			throws JSONException, IllegalAccessException,
			InvocationTargetException {
		JSONArray result = new JSONArray();

		Type[] argTypes = method.getGenericParameterTypes();

		for (int i = 0; i < argTypes.length; i++) {
			putObjectToJSONArray(argTypes[i], result, i,
					args[i]);
		}
		return result;
	}

	/** The Constant HEX. */
	private static final char[] HEX = { '0', '1', '2', '3', '4', '5', '6', '7',
			'8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

	/**
	 * Encode bytes as hex string.
	 *
	 * @param bytes the bytes
	 * @return the string
	 */
	protected static String toHexString(byte[] bytes) {
		int length = bytes.length;
		StringBuffer sb = new StringBuffer(length * 2);
		int x = 0;
		int n1 = 0, n2 = 0;
		for (int i = 0; i < length; i++) {
			if (bytes[i] >= 0)
				x = bytes[i];
			else
				x = 256 + bytes[i];
			n1 = x >> 4;
			n2 = x & 0x0f;
			sb = sb.append(HEX[n1]);
			sb = sb.append(HEX[n2]);
		}
		return sb.toString();
	}

	/**
	 * Stack2string.
	 *
	 * @param e the e
	 * @return the string
	 */
	protected static String stack2string(Exception e) {
		PrintWriter pw = null;
		try {
			StringWriter sw = new StringWriter();
			pw = new PrintWriter(sw);
			e.printStackTrace(pw);
			return "------\r\n" + sw.toString() + "------\r\n";
		} catch (Exception e2) {
			return "bad stack2string";
		} finally {
			if (pw != null) {
				try {
					pw.close();
				} catch (Exception ex) {
				}
			}
		}
	}

}
